# Copyright (C) 2015, Wazuh Inc.
# Created by Wazuh, Inc. <info@wazuh.com>.
# This program is free software; you can redistribute it and/or modify it under the terms of GPLv2

import contextlib
import operator
from os import chmod, chown, path
from typing import Optional, Union

from wazuh import __version__
from wazuh.core import common, configuration
from wazuh.core.agent import (
    Agent,
    WazuhDBQueryAgents,
    WazuhDBQueryGroupByAgents,
    create_upgrade_tasks,
    get_agents_info,
    get_groups,
    get_rbac_filters,
)
from wazuh.core.cluster.cluster import get_node
from wazuh.core.cluster.utils import read_cluster_config
from wazuh.core.exception import WazuhError, WazuhException, WazuhInternalError, WazuhResourceNotFound
from wazuh.core.indexer import get_indexer_client
from wazuh.core.indexer.base import IndexerKey
from wazuh.core.InputValidator import InputValidator
from wazuh.core.results import AffectedItemsWazuhResult, WazuhResult
from wazuh.core.utils import (
    full_copy,
    get_hash,
    process_array,
    get_group_file_path
)
from wazuh.core.wazuh_queue import WazuhQueue
from wazuh.rbac.decorators import expose_resources

cluster_enabled = not read_cluster_config(from_import=True)['disabled']
node_id = get_node().get('node') if cluster_enabled else None

UPGRADE_CHUNK_SIZE = 500
UPGRADE_RESULT_CHUNK_SIZE = 97

# Error codes generated from upgrade socket error codes that should be excluded in upgrade functions
# 1819 -> The WPK for this platform is not available
# 1820 -> Upgrade procedure could not start. Agent already upgrading
# 1821 -> Remote upgrade is not available for this agent version
# 1822 -> Current agent version is greater or equal
ERROR_CODES_UPGRADE_SOCKET = [1819, 1820, 1821, 1822]

# Error codes generated from upgrade socket error codes that should be raised as bad request (400)
# The rest of codes generated by the socket will be raised as Wazuh internal errors (500)
# 1823 -> Upgrading an agent to a version higher than the manager requires the force flag
ERROR_CODES_UPGRADE_SOCKET_BAD_REQUEST = [1823]

# Error codes generated from upgrade socket error codes that should be excluded in get upgrade results
# 1813 -> No task in DB
ERROR_CODES_UPGRADE_SOCKET_GET_UPGRADE_RESULT = [1813]


def build_agents_query(agent_list: list, filters: dict) -> dict:
    """Build the query to filter agents.

    Parameters
    ----------
    agent_list : list
        List of agents ID's.
    filters : dict
        Defines required field filters. Format: {"field1":"value1", "field2":["value2","value3"]}.

    Returns
    -------
    dict
        The query with the given parameters.
    """
    # TODO: The query build should be improved in https://github.com/wazuh/wazuh/issues/25289

    LAST_LOGIN_KEY = 'last_login'

    query_filters = []
    if agent_list:
        query_filters.append({IndexerKey.TERMS: {'_id': agent_list}})

    if LAST_LOGIN_KEY in filters:
        query_filters.append(
            {IndexerKey.RANGE: {LAST_LOGIN_KEY: {IndexerKey.LTE: f"{IndexerKey.NOW}-{filters[LAST_LOGIN_KEY]}"}}}
        )
        filters.pop(LAST_LOGIN_KEY)

    for key, value in filters.items():
        query_filters.append({IndexerKey.TERM: {key: value}})

    return {
        IndexerKey.QUERY: {
            IndexerKey.BOOL: {
                IndexerKey.FILTER: query_filters
            }
        }
    }


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"], post_proc_func=None)
def get_distinct_agents(agent_list: list = None, offset: int = 0, limit: int = common.DATABASE_LIMIT, sort: dict = None,
                        search: dict = None, fields: list = None, q: str = None) -> AffectedItemsWazuhResult:
    """Get all the different combinations that all system agents have for the selected fields. It also indicates the
    total number of agents that have each combination.

    Parameters
    ----------
    agent_list : list
        List of agents ID's.
    fields : list
        List of fields to group by.
    offset : int
        First item to return.
    limit : int
        Maximum number of items to return.
    sort : dict
        Sorts the items. Format: {"fields":["field1","field2"],"order":"asc|desc"}.
    search : dict
        Looks for items with the specified string. Format: {"fields": ["field1","field2"]}.
    q : str
        Query to filter results by. For example q&#x3D;&amp;quot;status&#x3D;active&amp;quot;

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """

    result = AffectedItemsWazuhResult(all_msg='All selected agents information was returned',
                                      some_msg='Some agents information was not returned',
                                      none_msg='No agent information was returned'
                                      )

    if agent_list:
        rbac_filters = get_rbac_filters(system_resources=get_agents_info(), permitted_resources=agent_list)

        with WazuhDBQueryGroupByAgents(filter_fields=fields, select=fields, offset=offset, limit=limit, sort=sort,
                                       search=search, query=q, min_select_fields=set(), count=True,
                                       get_data=True, **rbac_filters) as db_query:
            data = db_query.run()

        result.affected_items.extend(data['items'])
        result.total_affected_items = data['totalItems']

    return result


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"], post_proc_func=None)
def get_agents_summary_status(agent_list: list[str] = None) -> WazuhResult:
    """Count the number of agents by connection and groups configuration synchronization statuses.

    Parameters
    ----------
    agent_list : list[str]
       List of agents ID's

    Returns
    -------
    WazuhResult
        WazuhResult object.
    """
    connection = {'active': 0, 'disconnected': 0, 'never_connected': 0, 'pending': 0, 'total': 0}
    sync_configuration = {'synced': 0, 'not synced': 0, 'total': 0}
    if agent_list:
        rbac_filters = get_rbac_filters(system_resources=get_agents_info(), permitted_resources=agent_list)

        with WazuhDBQueryAgents(limit=None, select=['status', 'group_config_status'],
                                **rbac_filters) as db_query:
            data = db_query.run()

        items = data['items']
        for agent in items:
            connection[agent['status']] += 1
            sync_configuration[agent['group_config_status']] += 1

        connection['total'] = sync_configuration['total'] = len(items)

    sync_configuration['not_synced'] = sync_configuration.pop('not synced')
    return WazuhResult({'data': {'connection': connection, 'configuration': sync_configuration}})


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"], post_proc_func=None)
def get_agents_summary_os(agent_list: list[str] = None) -> AffectedItemsWazuhResult:
    """Get a list of available OS.

    Parameters
    ----------
    agent_list : list[str]
       List of agents ID's

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(none_msg='Could not get the operative system of the agents',
                                      all_msg='Showing the operative system of all specified agents',
                                      some_msg='Could not get the operative system of some agents')
    if agent_list:
        rbac_filters = get_rbac_filters(system_resources=get_agents_info(), permitted_resources=agent_list)

        with WazuhDBQueryAgents(select=['os.platform'], default_sort_field='os_platform', min_select_fields=set(),
                                distinct=True, **rbac_filters) as db_query:
            query_data = db_query.run()

        query_data['items'] = [row['os']['platform'] for row in query_data['items']]
        result.affected_items = query_data['items']
        result.total_affected_items = len(result.affected_items)

    return result


@expose_resources(actions=["agent:reconnect"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1707]})
def reconnect_agents(agent_list: Union[list, str] = None) -> AffectedItemsWazuhResult:
    """Force reconnect a list of agents.

    Parameters
    ----------
    list or str
        List of agent IDs. All possible values from 001 onwards. Default `*`

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(all_msg='Force reconnect command was sent to all agents',
                                      some_msg='Force reconnect command was not sent to some agents',
                                      none_msg='Force reconnect command was not sent to any agent'
                                      )

    system_agents = get_agents_info()
    with WazuhQueue(common.AR_SOCKET) as wq:
        for agent_id in agent_list:
            try:
                if agent_id not in system_agents:
                    raise WazuhResourceNotFound(1701)
                Agent(agent_id).reconnect(wq)
                result.affected_items.append(agent_id)
            except WazuhException as e:
                result.add_failed_item(id_=agent_id, error=e)

    result.total_affected_items = len(result.affected_items)
    result.affected_items.sort(key=int)

    return result


@expose_resources(actions=["agent:restart"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1703, 1707]})
async def restart_agents(agent_list: list) -> AffectedItemsWazuhResult:
    """Restart a list of agents.

    Parameters
    ----------
    agent_list : list
        List of agents IDs.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(
        all_msg='Restart command was sent to all agents',
        some_msg='Restart command was not sent to some agents',
        none_msg='Restart command was not sent to any agent'
    )

    raise NotImplementedError('Implement once the restart endpoint is defined')


@expose_resources(actions=['cluster:read'], resources=[f'node:id:{node_id}'],
                  post_proc_kwargs={'exclude_codes': [1701, 1707], 'force': True})
def restart_agents_by_node(agent_list: list = None) -> AffectedItemsWazuhResult:
    """Restart all agents belonging to a node.

    Parameters
    ----------
    agent_list : list, optional
        List of agents. Default `None`

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    return restart_agents(agent_list=agent_list)


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1707], 'force': True})
def restart_agents_by_group(agent_list: list = None) -> AffectedItemsWazuhResult:
    """Restart all agents belonging to a group.

    Parameters
    ----------
    agent_list : list, optional
        List of agents. Default `None`.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    return restart_agents(agent_list=agent_list)


@expose_resources(
    actions=["agent:read"], resources=["agent:id:{agent_list}"], post_proc_kwargs={'exclude_codes': [1701]}
)
async def get_agents(
    agent_list: list,
    filters: Optional[dict] = None,
    offset: int = 0,
    limit: int = common.DATABASE_LIMIT,
    sort: dict = None,
    select: dict = None,
) -> AffectedItemsWazuhResult:
    """Gets a list of available agents with basic attributes.

    Parameters
    ----------
    agent_list : list
        List of agent UUIDs to filter.
    filters : dict
        Defines required field filters. Format: {"field1":"value1", "field2":["value2","value3"]}.
    offset : int
        First element to return in the collection.
    limit : int
        Maximum number of elements to return. Default: common.DATABASE_LIMIT
    select : dict
        Select fields to return. Format: {"fields":["field1","field2"]}.
    sort : dict
        Sorts the items. Format: {"fields":["field1","field2"],"order":"asc|desc"}.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(
        all_msg='All selected agents information was returned',
        some_msg='Some agents information was not returned',
        none_msg='No agent information was returned'
    )
    if filters is None:
        filters = dict()

    query = build_agents_query(agent_list, filters)

    async with get_indexer_client() as indexer:
        items = await indexer.agents.search(query, select=select, exclude='key', limit=limit, offset=offset, sort=sort)

    result.affected_items.extend(items)
    result.total_affected_items = len(items)

    return result


@expose_resources(actions=["group:read"], resources=["group:id:{group_list}"], post_proc_func=None)
async def get_agents_in_group(group_list: list, offset: int = 0, limit: int = None, sort_by: list = None,
                     sort_ascending: bool = True, search_text: str = None, complementary_search: bool = False,
                     q: str = None, select: str = None, distinct: bool = False) -> AffectedItemsWazuhResult:
    """Gets the list of agents that belong to a specific group.

    Parameters
    ----------
    group_list : list
        List containing the group ID.
    offset : int
        First element to return in the collection.
    limit : int
        Maximum number of elements to return. Default: common.DATABASE_LIMIT
    sort_by : list
        Fields to sort the items by.
    sort_ascending : bool
        Sort in ascending (true) or descending (false) order. Default: True
    search_text : str
        Text to search.
    complementary_search : bool
        Find items without the text to search. Default: False
    q : str
        Query to filter results by.
    select : str
        Select which fields to return (separated by comma).
    distinct : bool
        Look for distinct values.

    Raises
    ------
    WazuhResourceNotFound(1710)
        If the group does not exist.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(all_msg='All selected groups information was returned',
                                      some_msg='Some groups information was not returned',
                                      none_msg='No group information was returned'
                                      )

    group_id = group_list[0]
    system_groups = get_groups()

    if group_id not in system_groups:
        raise WazuhResourceNotFound(1710)

    async with get_indexer_client() as indexer_client:
        agents = await indexer_client.agents.get_group_agents(group_id)

    data = process_array(agents, offset=offset, limit=limit, sort_by=sort_by, sort_ascending=sort_ascending,
            search_text=search_text, complementary_search=complementary_search, q=q, select=select,
            distinct=distinct, allowed_select_fields=Agent.new_fields, allowed_sort_fields=Agent.new_fields)

    result.affected_items = data['items']
    result.total_affected_items = data['totalItems']

    return result


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701]})
def get_agents_keys(agent_list: list = None) -> AffectedItemsWazuhResult:
    """Get the key of existing agents.

    Parameters
    ----------
    agent_list : list
        List of agents ID's.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(all_msg='Obtained keys for all selected agents',
                                      some_msg='Some agent keys were not obtained',
                                      none_msg='No agent keys were obtained'
                                      )
    system_agents = get_agents_info()
    for agent_id in agent_list:
        try:
            if agent_id not in system_agents:
                raise WazuhResourceNotFound(1701)
            result.affected_items.append({'id': agent_id, 'key': Agent(agent_id).get_key()})
        except WazuhException as e:
            result.add_failed_item(id_=agent_id, error=e)
    result.total_affected_items = len(result.affected_items)
    result.affected_items.sort(key=lambda i: i['id'])

    return result


@expose_resources(actions=["agent:delete"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1703, 1731]})
async def delete_agents(agent_list: list, filters: Optional[dict] = None,) -> AffectedItemsWazuhResult:
    """Delete a list of agents or all of them if receive an empty list.

    Parameters
    ----------
    agent_list : list
        List of agents ID's to be deleted.
    filters : dict
        Defines required field filters. Format: {"field1":"value1", "field2":["value2","value3"]}.

    Returns
    -------
    AffectedItemsWazuhResult
        Result with affected agents.
    """
    result = AffectedItemsWazuhResult(
        all_msg='All selected agents were deleted',
        some_msg='Some agents were not deleted',
        none_msg='No agents were deleted'
    )

    async with get_indexer_client() as indexer:
        available_agents = [item.id for item in
            await indexer.agents.search(query=build_agents_query(agent_list, filters))
        ]
        not_found_agents = set(agent_list) - set(available_agents)

        for not_found_id in not_found_agents:
            result.add_failed_item(not_found_id, error=WazuhResourceNotFound(1701))

        agent_list = available_agents

        deleted_items = await indexer.agents.delete(agent_list)

    result.affected_items = deleted_items
    result.total_affected_items = len(result.affected_items)

    return result


@expose_resources(actions=["agent:create"], resources=["*:*:*"], post_proc_func=None)
async def add_agent(id: str, name: str, key: str, groups: str = None) -> WazuhResult:
    """Add a new Wazuh agent.

    Parameters
    ----------
    id : str
        New agent ID.
    name : str
        New agent name.
    key : str
        New agent key.
    groups : str
        New agent groups.

    Raises
    ------
    WazuhError(1738)
        Name length is greater than 128 characters.

    Returns
    -------
    WazuhResult
        Added agent information.
    """
    # Check length of agent name
    if len(name) > common.AGENT_NAME_LEN_LIMIT:
        raise WazuhError(1738)

    async with get_indexer_client() as indexer:
        new_agent = await indexer.agents.create(id=id, name=name, key=key, groups=groups)

    return WazuhResult({'data': new_agent})


@expose_resources(actions=["group:read"], resources=["group:id:{group_list}"],
                  post_proc_kwargs={'exclude_codes': [1710]})
def get_agent_groups(group_list: list = None, offset: int = 0, limit: int = None, sort_by: list = None,
                     sort_ascending: bool = True, search_text: str = None, complementary_search: bool = False,
                     hash_algorithm: str = 'md5', q: str = None, select: str = None,
                     distinct: bool = False) -> AffectedItemsWazuhResult:
    """Gets the existing groups.

    Parameters
    ----------
    group_list : list
        List of group names.
    offset : int
        First element to return in the collection.
    limit : int
        Maximum number of elements to return. Default: common.DATABASE_LIMIT
    sort_by : list
        Fields to sort the items by.
    sort_ascending : bool
        Sort in ascending (true) or descending (false) order. Default: True
    search_text : str
        Text to search.
    complementary_search : bool
        Find items without the text to search. Default: False
    hash_algorithm : str
        hash algorithm used to get mergedsum and configsum. Default: 'md5'
    q : str
        Query to filter results by.
    select : str
        Select which fields to return (separated by comma).
    distinct : bool
        Look for distinct values.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    affected_groups = list()
    result = AffectedItemsWazuhResult(all_msg='All selected groups information was returned',
                                      some_msg='Some groups information was not returned',
                                      none_msg='No group information was returned'
                                      )

    if group_list:
        system_groups = get_groups()

        # Add failed items
        for invalid_group in set(group_list) - system_groups:
            group_list.remove(invalid_group)
            result.add_failed_item(id_=invalid_group, error=WazuhResourceNotFound(1710))

        for name in group_list:
            group = {'name': name}

            conf_sum = get_hash(get_group_file_path(name), hash_algorithm)
            if conf_sum:
                group['configSum'] = conf_sum

            affected_groups.append(group)

        data = process_array(affected_groups, offset=offset, limit=limit,
                            sort_by=sort_by, sort_ascending=sort_ascending, search_text=search_text,
                            complementary_search=complementary_search, q=q,
                            select=select, distinct=distinct)
        result.affected_items = data['items']
        result.total_affected_items = data['totalItems']

    return result


@expose_resources(actions=["group:create"], resources=["*:*:*"], post_proc_func=None)
def create_group(group_id: str) -> WazuhResult:
    """Creates a group.

    Parameters
    ----------
    group_id : str
        Group ID.

    Raises
    ------
    WazuhError(1722)
        If there was a validation error.
    WazuhError(1711)
        If the group already exists.
    WazuhError(1713)
        If the group ID is not valid.
    WazuhInternalError(1005)
        If there was an error reading a file.

    Returns
    -------
    WazuhResult
        WazuhResult object with a operation message.
    """
    # Input Validation of group_id
    if not InputValidator().group(group_id):
        raise WazuhError(1722)

    if group_id.lower() == "agent-template":
        raise WazuhError(1713, extra_message=group_id)

    group_path = get_group_file_path(group_id)

    if group_id.lower() == "default" or path.exists(group_path):
        raise WazuhError(1711, extra_message=group_id)

    # Create group in /etc/shared
    agent_conf_template = path.join(common.SHARED_PATH, 'agent-template.conf')
    try:
        full_copy(agent_conf_template, group_path)

        chown(group_path, common.wazuh_uid(), common.wazuh_gid())
        chmod(group_path, 0o660)
        msg = f"Group '{group_id}' created."
    except Exception as e:
        raise WazuhInternalError(1005, extra_message=str(e))

    return WazuhResult({'message': msg})


@expose_resources(actions=["group:delete"], resources=["group:id:{group_list}"],
                  post_proc_kwargs={'exclude_codes': [1710, 1712]})
async def delete_groups(group_list: list = None) -> AffectedItemsWazuhResult:
    """Delete a list of groups and remove it from every agent assignations.

    Parameters
    ----------
    group_list : list
        List of Group names.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    result = AffectedItemsWazuhResult(all_msg='All selected groups were deleted',
                                      some_msg='Some groups were not deleted',
                                      none_msg='No group was deleted')

    system_groups = get_groups()
    for group_id in group_list:
        try:
            # Check if group exists
            if group_id not in system_groups:
                raise WazuhResourceNotFound(1710)
            elif group_id == 'default':
                raise WazuhError(1712)

            await Agent.delete_single_group(group_id)
            result.affected_items.append(group_id)
        except WazuhException as e:
            result.add_failed_item(id_=group_id, error=e)

    result.total_affected_items = len(result.affected_items)

    return result


@expose_resources(actions=["group:modify_assignments"], resources=['group:id:{replace_list}'], post_proc_func=None)
@expose_resources(actions=["group:modify_assignments"], resources=['group:id:{group_list}'], post_proc_func=None)
@expose_resources(actions=["agent:modify_group"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1751, 1752]})
async def assign_agents_to_group(group_list: list = None, agent_list: list = None, replace: bool = False,
                           replace_list: list = None) -> AffectedItemsWazuhResult:
    """Assign a list of agents to a group.

    Parameters
    ----------
    group_list : list
        List with the group ID.
    agent_list : list
        List of Agent IDs.
    replace :  bool
        Whether to append new group to current agent's group or replace it.
    replace_list : list
        List of Group names that can be replaced.

    Raises
    ------
    WazuhResourceNotFound(1710)
        If the group was not found.


    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    group_id = group_list[0]
    result = AffectedItemsWazuhResult(all_msg=f'All selected agents were assigned to {group_id}'
                                              f'{" and removed from the other groups" if replace else ""}',
                                      some_msg=f'Some agents were not assigned to {group_id}'
                                               f'{" and removed from the other groups" if replace else ""}',
                                      none_msg='No agents were assigned to {0}'.format(group_id)
                                      )
    # Check if the group exists
    if not Agent.group_exists(group_id):
        raise WazuhResourceNotFound(1710)

    if len(agent_list) != 0:
        query = {IndexerKey.TERMS: {IndexerKey._ID: agent_list}}
    else:
        query = {IndexerKey.MATCH_ALL: {}}

    async with get_indexer_client() as indexer_client:
        available_agents = [item.id for item in
            await indexer_client.agents.search(query={IndexerKey.QUERY: query})
        ]

        for not_found_id in set(agent_list) - set(available_agents):
            result.add_failed_item(not_found_id, error=WazuhResourceNotFound(1701))
            agent_list.remove(not_found_id)

        await indexer_client.agents.add_agents_to_group(group_name=group_id, agent_ids=agent_list)

    result.affected_items.extend(agent_list)
    result.total_affected_items = len(result.affected_items)

    return result


@expose_resources(actions=["agent:modify_group"], resources=["agent:id:{agent_list}"], post_proc_func=None)
@expose_resources(actions=["group:modify_assignments"], resources=["group:id:{group_list}"],
                  post_proc_kwargs={'exclude_codes': [1710, 1734, 1745]})
async def remove_agent_from_groups(agent_list: list = None, group_list: list = None) -> AffectedItemsWazuhResult:
    """Removes an agent assignation with a list of groups.

    Parameters
    ----------
    group_list : list
        List of groups IDs.
    agent_list : list
        List with the agent ID.

    Raises
    ------
    WazuhResourceNotFound(1701)
        Agent was not found.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    agent_id = agent_list[0]
    result = AffectedItemsWazuhResult(all_msg='Specified agent was removed from returned groups',
                                      some_msg='Specified agent was not removed from some groups',
                                      none_msg='Specified agent was not removed from any group'
                                      )

    # Check if agent exists
    _ = await Agent.get(agent_id)

    # We move default group to last position in case it is contained in group_list. When an agent is removed from all
    # groups it is reverted to 'default'. We try default last to avoid removing it and then adding again.
    with contextlib.suppress(ValueError):
        group_list.append(group_list.pop(group_list.index('default')))

    system_groups = get_groups()
    for group_id in group_list:
        try:
            if group_id not in system_groups:
                raise WazuhResourceNotFound(1710)
            await Agent.unset_single_group_agent(agent_id=agent_id, group_id=group_id, force=True)
            result.affected_items.append(group_id)
        except WazuhException as e:
            result.add_failed_item(id_=group_id, error=e)
    result.total_affected_items = len(result.affected_items)
    result.affected_items.sort()

    return result


@expose_resources(actions=["group:modify_assignments"], resources=["group:id:{group_list}"], post_proc_func=None)
@expose_resources(actions=["agent:modify_group"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1734]})
async def remove_agents_from_group(agent_list: list = None, group_list: list = None) -> AffectedItemsWazuhResult:
    """Remove the assignations of a list of agents with a specified group.

    Parameters
    ----------
    group_list : list
        List with the group ID.
    agent_list : list
        List of Agent IDs.

    Raises
    ------
    WazuhResourceNotFound(1710)
        Group was not found.

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """
    group_id = group_list[0]
    result = AffectedItemsWazuhResult(all_msg=f'All selected agents were removed from group {group_id}',
                                      some_msg=f'Some agents were not removed from group {group_id}',
                                      none_msg=f'No agent was removed from group {group_id}'
                                      )

    system_groups = get_groups()
    if group_id not in system_groups:
        raise WazuhResourceNotFound(1710)

    if len(agent_list) != 0:
        query = {IndexerKey.TERMS: {IndexerKey._ID: agent_list}}
    else:
        query = {IndexerKey.MATCH_ALL: {}}

    async with get_indexer_client() as indexer_client:
        available_agents = [item.id for item in
            await indexer_client.agents.search(query={IndexerKey.QUERY: query})
        ]

        for not_found_id in set(agent_list) - set(available_agents):
            result.add_failed_item(not_found_id, error=WazuhResourceNotFound(1701))
            agent_list.remove(not_found_id)

        await indexer_client.agents.remove_agents_from_group(group_name=group_id, agent_ids=agent_list)

    result.affected_items.extend(agent_list)
    result.total_affected_items = len(result.affected_items)

    return result


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"], post_proc_func=None)
def get_outdated_agents(agent_list: list = None, offset: int = 0, limit: int = common.DATABASE_LIMIT, sort: dict = None,
                        search: dict = None, select: str = None, q: str = None) -> AffectedItemsWazuhResult:
    """Gets the outdated agents.

    Parameters
    ----------
    agent_list : list
        List of agents ID's.
    offset : int
        First item to return.
    limit : int
        Maximum number of items to return. Default: common.DATABASE_LIMIT
    sort : dict
        Sorts the items. Format: {"fields":["field1","field2"],"order":"asc|desc"}.
    search : dict
        Looks for items with the specified string. Format: {"fields": ["field1","field2"]}.
    select : str
        Select which fields to return (separated by comma).
    q : str
        Query to filter results by. For example q&#x3D;&amp;quot;status&#x3D;active&amp;quot;

    Returns
    -------
    AffectedItemsWazuhResult
        Affected items.
    """

    result = AffectedItemsWazuhResult(all_msg='All selected agents information was returned',
                                      some_msg='Some agents information was not returned',
                                      none_msg='No agent information was returned'
                                      )
    if agent_list:

        rbac_filters = get_rbac_filters(system_resources=get_agents_info(), permitted_resources=agent_list)

        with WazuhDBQueryAgents(offset=offset, limit=limit, sort=sort, search=search, select=select,
                                query=f"version!=v{__version__}" + (';' + q if q else ''),
                                **rbac_filters) as db_query:
            data = db_query.run()

        result.affected_items = data['items']
        result.total_affected_items = data['totalItems']

    return result


@expose_resources(actions=["agent:upgrade"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs={'exclude_codes': [1701, 1707, 1731] + ERROR_CODES_UPGRADE_SOCKET})
def upgrade_agents(agent_list: list = None, wpk_repo: str = None, version: str = None, force: bool = False,
                   use_http: bool = False, package_type: str = None, file_path: str = None, installer: str = None,
                   filters: dict = None, q: str = None) -> AffectedItemsWazuhResult:
    """Start the agent upgrade process.

    Parameters
    ----------
    agent_list : list
        List of agents ID's.
    wpk_repo : str
        URL for WPK download.
    version : str
        Version to upgrade to.
    force : bool
        Forces the agents to upgrade, ignoring version validations.
    use_http : bool
        False for HTTPS protocol, True for HTTP protocol.
    package_type : str
        Default package type (rpm, deb).
    file_path : str
        Path to the installation file.
    installer : str
        Selected installer.
    filters : dict
        Define required field filters. Format: {"field1":"value1", "field2":["value2","value3"]}
    q : str
        Define query to filter in DB.

    Raises
    ------
    WazuhError(1823)
        If the target version is higher than the manager's.
    WazuhInternalError
        Generic internal server error.

    Returns
    -------
    AffectedItemsWazuhResult
        Result with IDs of created tasks.
    """
    result = AffectedItemsWazuhResult(all_msg='All upgrade tasks were created',
                                      some_msg='Some upgrade tasks were not created',
                                      none_msg='No upgrade task was created',
                                      sort_fields=['agent'], sort_ascending='True')

    if agent_list:
        system_agents = get_agents_info()
        rbac_filters = get_rbac_filters(system_resources=system_agents, permitted_resources=agent_list,
                                        filters=filters)

        with WazuhDBQueryAgents(limit=None, select=["id", "status"], query=q, **rbac_filters) as db_query:
            data = db_query.run()

        filtered_agents = set([agent['id'] for agent in data['items']])
        agent_list = set(agent_list)

        # Add non existent agents to failed_items
        not_found_agents = agent_list - system_agents
        [result.add_failed_item(id_=agent, error=WazuhResourceNotFound(1701)) for agent in not_found_agents]

        # Add non active agents to failed_items
        non_active_agents = [agent['id'] for agent in data['items'] if agent['status'] != 'active']
        [result.add_failed_item(id_=agent, error=WazuhError(1707)) for agent in non_active_agents]
        non_active_agents = set(non_active_agents)

        # Add non eligible agents to failed_items
        non_eligible_agents = agent_list - not_found_agents - non_active_agents - filtered_agents
        [result.add_failed_item(id_=ag, error=WazuhError(
            1731,
            extra_message="some of the requirements are not met -> {}".format(
                ', '.join(f"{key}: {value}" for key, value in filters.items() if key != 'rbac_ids') +
                (f', q: {q}' if q else '')
            )
        )) for ag in non_eligible_agents]

        eligible_agents = agent_list - not_found_agents - non_active_agents - non_eligible_agents

        # Transform the format of the agent ids to the general format
        eligible_agents = [int(agent) for agent in eligible_agents]

        tasks_results = create_upgrade_tasks(eligible_agents=eligible_agents, chunk_size=UPGRADE_CHUNK_SIZE,
                                             command='upgrade' if not (installer or file_path) else 'upgrade_custom',
                                             wpk_repo=wpk_repo, version=version, force=force, use_http=use_http,
                                             package_type=package_type, file_path=file_path, installer=installer)

        for agent_result_chunk in tasks_results:
            for agent_result in agent_result_chunk['data']:
                socket_error = agent_result['error']
                # Success, return agent and task IDs
                if socket_error == 0:
                    task_agent = {
                        'agent': str(agent_result['agent']).zfill(3),
                        'task_id': agent_result['task_id']
                    }
                    result.affected_items.append(task_agent)
                    result.total_affected_items += 1

                # Upgrade error for specific agents
                elif (error_code := 1810 + socket_error) in ERROR_CODES_UPGRADE_SOCKET:
                    error = WazuhError(error_code, cmd_error=True, extra_message=agent_result['message'])
                    result.add_failed_item(id_=str(agent_result['agent']).zfill(3), error=error)

                # Upgrade error for all agents, bad request
                elif error_code in ERROR_CODES_UPGRADE_SOCKET_BAD_REQUEST:
                    raise WazuhError(error_code, cmd_error=True, extra_message=agent_result['message'])

                # Upgrade error for all agents, internal server error
                else:
                    raise WazuhInternalError(error_code, cmd_error=True, extra_message=agent_result['message'])

    result.affected_items.sort(key=operator.itemgetter('agent'))

    return result


@expose_resources(actions=["agent:upgrade"], resources=["agent:id:{agent_list}"],
                  post_proc_kwargs=
                  {'exclude_codes': [1701, 1707, 1731] + ERROR_CODES_UPGRADE_SOCKET_GET_UPGRADE_RESULT})
def get_upgrade_result(agent_list: list = None, filters: dict = None, q: str = None) -> AffectedItemsWazuhResult:
    """Read upgrade result output from agent.

    Parameters
    ----------
    agent_list : list
        List of agent ID's.
    filters : dict
        Define required field filters. Format: {"field1":"value1", "field2":["value2","value3"]}
    q : str
        Define query to filter in DB.

    Raises
    ------
    WazuhInternalError
        Generic internal server error.

    Returns
    -------
    AffectedItemsWazuhResult
        Upgrade results.
    """
    result = AffectedItemsWazuhResult(all_msg='All upgrade tasks were returned',
                                      some_msg='Some upgrade tasks were not returned',
                                      none_msg='No upgrade task was returned')

    if agent_list:
        system_agents = get_agents_info()
        rbac_filters = get_rbac_filters(system_resources=system_agents, permitted_resources=agent_list,
                                        filters=filters)

        with WazuhDBQueryAgents(limit=None, select=["id", "status"], query=q, **rbac_filters) as db_query:
            data = db_query.run()

        filtered_agents = set([agent['id'] for agent in data['items']])
        agent_list = set(agent_list)

        # Add non existent agents to failed_items
        not_found_agents = agent_list - system_agents
        [result.add_failed_item(id_=agent, error=WazuhResourceNotFound(1701)) for agent in not_found_agents]

        # Add non active agents to failed_items
        non_active_agents = [agent['id'] for agent in data['items'] if agent['status'] != 'active']
        [result.add_failed_item(id_=agent, error=WazuhError(1707)) for agent in non_active_agents]
        non_active_agents = set(non_active_agents)

        # Add non eligible agents to failed_items
        non_eligible_agents = agent_list - not_found_agents - non_active_agents - filtered_agents
        [result.add_failed_item(id_=ag, error=WazuhError(
            1731,
            extra_message="some of the requirements are not met -> {}".format(
                ', '.join(f"{key}: {value}" for key, value in filters.items() if key != 'rbac_ids') +
                (f', q: {q}' if q else '')
            )
        )) for ag in non_eligible_agents]

        eligible_agents = agent_list - not_found_agents - non_active_agents - non_eligible_agents

        # Transform the format of the agent ids to the general format
        eligible_agents = [int(agent) for agent in eligible_agents]

        task_results = create_upgrade_tasks(eligible_agents=eligible_agents, chunk_size=UPGRADE_RESULT_CHUNK_SIZE,
                                            command='upgrade_result', get_result=True)

        for task_result_chunk in task_results:
            for task_result in task_result_chunk['data']:
                task_error = task_result.pop('error')
                # Success, return agent and task IDs
                if task_error == 0:
                    task_result['agent'] = str(task_result['agent']).zfill(3)
                    result.affected_items.append(task_result)
                    result.total_affected_items += 1

                # Upgrade error for specific agents (no task in DB)
                elif (error_code := 1810 + task_error) in ERROR_CODES_UPGRADE_SOCKET_GET_UPGRADE_RESULT:
                    error = WazuhError(error_code, cmd_error=True, extra_message=task_result['message'])
                    result.add_failed_item(id_=str(task_result['agent']).zfill(3), error=error)

                # Upgrade error for all agents, internal server error
                else:
                    raise WazuhInternalError(error_code, cmd_error=True, extra_message=task_result['message'])

    result.affected_items.sort(key=operator.itemgetter('agent'))

    return result


@expose_resources(actions=["agent:read"], resources=["agent:id:{agent_list}"], post_proc_func=None)
def get_agent_config(agent_list: list = None, component: str = None, config: str = None) -> WazuhResult:
    """Read selected configuration from agent.

    Parameters
    ----------
    agent_list : list
        List of agents ID's.
    component : str
        Selected component.
    config : str
        Configuration to get, written on disk.

    Raises
    ------
    WazuhError(1740)
        If the agent is not active.

    Returns
    -------
    WazuhResult
        Loaded configuration in JSON.
    """
    # We access unique agent_id from list, this may change if and when we decide a final way to handle get responses
    # with failed ids and a list of agents
    agent_id = agent_list[0]
    my_agent = Agent(agent_id)
    my_agent.load_info_from_db()

    if my_agent.status != "active":
        raise WazuhError(1740)

    return WazuhResult(
        {'data': my_agent.get_config(component=component, config=config, agent_version=my_agent.version)})


@expose_resources(actions=["group:read"], resources=["group:id:{group_list}"], post_proc_func=None)
def get_group_conf(group_list: list = None) -> WazuhResult:
    """Read the configuration of the specified group.

    Parameters
    ----------
    group_list : list
        List with the group ID.

    Returns
    -------
    WazuhResult
        WazuhResult object with the configuration.
    """
    # We access unique group_id from list, this may change if and when we decide to add option to get agent conf for
    # a list of groups
    group_id = group_list[0]

    return WazuhResult({'data': configuration.get_group_conf(group_id=group_id)})


@expose_resources(actions=["group:update_config"], resources=["group:id:{group_list}"], post_proc_func=None)
def update_group_file(group_list: list = None, file_data: str = None) -> WazuhResult:
    """Update a group file.

    Parameters
    ----------
    group_list : list
        List with the group ID.
    file_data : str
        Relative path of temporary file to upload.

    Returns
    -------
    WazuhResult
        WazuhResult object with the confirmation message.
    """
    # We access unique group_id from list, this may change if and when we decide to add option to update files for
    # a list of groups
    group_id = group_list[0]

    return WazuhResult({'message': configuration.update_group_file(group_id, file_data)})


def get_full_overview() -> WazuhResult:
    """Get information about agents.

    Returns
    -------
    WazuhResult
        WazuhResult object with information about agents.
    """
    # Get information from different methods of Agent class
    stats_distinct_node = get_distinct_agents(fields=['node_name']).affected_items
    groups = get_agent_groups().affected_items
    stats_distinct_os = get_distinct_agents(fields=['os.name',
                                                    'os.platform', 'os.version']).affected_items
    stats_version = get_distinct_agents(fields=['version']).affected_items
    agent_summary_status = get_agents_summary_status()
    summary = agent_summary_status['data'] if 'data' in agent_summary_status else dict()
    try:
        last_registered_agent = [get_agents(limit=1,
                                            sort={'fields': ['dateAdd'], 'order': 'desc'}).affected_items[0]]
    except IndexError:  # an IndexError could happen if there are not registered agents
        last_registered_agent = []
    # combine results in an unique dictionary
    result = {'nodes': stats_distinct_node, 'groups': groups, 'agent_os': stats_distinct_os, 'agent_status': summary,
              'agent_version': stats_version, 'last_registered_agent': last_registered_agent}

    return WazuhResult({'data': result})
